package com.biz.primus.ms.coupon.service;

import com.biz.primus.base.exception.utils.AssertUtils;
import com.biz.primus.common.utils.ValueUtils;
import com.biz.primus.model.coupon.enums.CouponLimitType;
import com.biz.primus.model.coupon.enums.CouponScopeType;
import com.biz.primus.model.coupon.enums.CouponState;
import com.biz.primus.model.coupon.exceptions.CouponExceptionType;
import com.biz.primus.model.coupon.exceptions.CouponTypeExceptionType;
import com.biz.primus.model.coupon.vo.coupon.CouponVo;
import com.biz.primus.model.coupon.vo.coupon.GetOrderProductAvailableCouponProductItemVo;
import com.biz.primus.model.coupon.vo.coupon.UserCouponListVo;
import com.biz.primus.model.coupon.vo.coupon.request.CreateCouponReqVo;
import com.biz.primus.model.coupon.vo.coupon.request.GetCouponCutAmountReqVo;
import com.biz.primus.model.coupon.vo.coupon.request.GetOrderProductAvailableCouponReqVo;
import com.biz.primus.model.coupon.vo.coupon.request.UseCouponReqVo;
import com.biz.primus.ms.base.service.AbstractBaseService;
import com.biz.primus.ms.coupon.dao.po.CouponType;
import com.biz.primus.ms.coupon.dao.redis.CouponRedisDao;
import com.biz.primus.ms.coupon.dao.redis.CouponTypeRedisDao;
import com.biz.primus.ms.coupon.dao.repository.CouponTypeRepository;
import com.biz.primus.ms.coupon.dao.ro.CouponRo;
import com.biz.primus.ms.coupon.dao.ro.CouponTypeRo;
import com.biz.primus.ms.coupon.event.coupon.CreateCouponEvent;
import com.biz.primus.ms.coupon.trans.CouponRo2CouponVo;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;

/**
 * CouponServiceImpl
 *
 * @author david-liu
 */
@Service
public class CouponServiceImpl extends AbstractBaseService implements CouponService {

    @Autowired
    private ApplicationEventPublisher applicationEventPublisher;

    @Autowired
    private CouponTypeRedisDao couponTypeRedisDao;

    @Autowired
    private CouponRedisDao couponRedisDao;

    @Autowired
    private CouponTypeRepository couponTypeRepository;

    @Override
    public Boolean createCoupon(CreateCouponReqVo reqVo) {
        Long couponTypeId = reqVo.getCouponTypeId();
        List<Long> userIds = reqVo.getUserIds();
        CouponTypeRo couponTypeRo = couponTypeRedisDao.findOne(couponTypeId);
        CouponType couponType = couponTypeRepository.findOne(couponTypeId);
        AssertUtils.notNull(couponTypeRo, CouponTypeExceptionType.COUPON_TYPE_ILLEGAL_STATE);
        AssertUtils.notNull(couponType, CouponTypeExceptionType.COUPON_TYPE_ILLEGAL_STATE);
        Integer totalCreateCount = userIds.size() * reqVo.getQuantity();
        synchronized (this) {
            int remainCouponQuantity = couponRedisDao.getCouponTypeAvailableCouponCount(couponTypeId);
            AssertUtils.isTrue(remainCouponQuantity >= totalCreateCount, CouponExceptionType.COUPON_INSUFFICIENT, new Object[]{String.format("剩余优惠券[%d]张, 需要发优惠券[%d]张", remainCouponQuantity, totalCreateCount)});
            LocalDateTime now = LocalDateTime.now();
            DateTimeFormatter localDateTimeFormatter = DateTimeFormatter.ofPattern("yyyy年M月d日");
            AssertUtils.isTrue(couponTypeRo.isTimeInReceivableTimeRange(now), CouponTypeExceptionType.TIME_NOT_IN_RECEIVABLE_TIME_RANGE, new Object[]{String.format("优惠券开始领取时间为%s, 结束领取时间为%s", localDateTimeFormatter.format(couponTypeRo.getReceivableStartTime()), localDateTimeFormatter.format(couponTypeRo.getReceivableEndTime()))});
            List<CouponRo> couponRos = couponRedisDao.appendCoupon2UserNotUsedCouponList(userIds, couponTypeId, reqVo.getCouponAcquireApproach());
            applicationEventPublisher.publishEvent(new CreateCouponEvent(this, couponRos, couponType));
        }
        return true;
    }

    @Override
    public UserCouponListVo listUserCoupons(Long userId) {
        AssertUtils.notNull(userId, CouponExceptionType.COUPON_USER_NOT_ALLOW_NULL);
        UserCouponListVo couponListVo = new UserCouponListVo();
        List<CouponRo> userNotUsedCoupons = couponRedisDao.listUserNotUsedCoupons(userId);
        List<CouponRo> userUsedCoupons = couponRedisDao.listUserUsedCoupons(userId);
        List<CouponRo> userExpiredCoupons = couponRedisDao.listUserExpiredCoupons(userId);
        Set<Long> couponTypeIds = Sets.newLinkedHashSet();
        couponTypeIds.addAll(userNotUsedCoupons.stream().map(CouponRo::getCouponTypeId).collect(Collectors.toSet()));
        couponTypeIds.addAll(userUsedCoupons.stream().map(CouponRo::getCouponTypeId).collect(Collectors.toSet()));
        couponTypeIds.addAll(userExpiredCoupons.stream().map(CouponRo::getCouponTypeId).collect(Collectors.toSet()));
        Map<Long, CouponTypeRo> couponTypeRoMap = Maps.newHashMap();
        if (CollectionUtils.isNotEmpty(couponTypeIds)) {
            couponTypeRoMap = Optional.ofNullable(couponTypeRedisDao.findByIds(couponTypeIds)).orElse(Lists.newArrayList()).stream().collect(Collectors.toMap(CouponTypeRo::getId, Function.identity()));
        }

        if (CollectionUtils.isNotEmpty(userNotUsedCoupons)) {
            Map<Long, CouponTypeRo> finalCouponTypeRoMap = couponTypeRoMap;
            List<CouponVo> notUsedCouponVo = userNotUsedCoupons.stream().filter(ro -> {
                CouponTypeRo couponTypeRo = finalCouponTypeRoMap.get(ro.getCouponTypeId());
                return this.isCouponUsableNow(couponTypeRo, ro);
            }).map(ro -> new CouponRo2CouponVo(finalCouponTypeRoMap.get(ro.getCouponTypeId())).apply(ro)).filter(Objects::nonNull).collect(Collectors.toList());
            couponListVo.setUserNotUsedCoupons(notUsedCouponVo);
        }

        if (CollectionUtils.isNotEmpty(userUsedCoupons)) {
            Map<Long, CouponTypeRo> finalCouponTypeRoMap = couponTypeRoMap;
            List<CouponVo> usedCouponVo = userUsedCoupons.stream().filter(ro -> {
                CouponTypeRo couponTypeRo = finalCouponTypeRoMap.get(ro.getCouponTypeId());
                return this.isCouponUsableNow(couponTypeRo, ro);
            }).map(ro -> new CouponRo2CouponVo(finalCouponTypeRoMap.get(ro.getCouponTypeId())).apply(ro)).filter(Objects::nonNull).collect(Collectors.toList());
            couponListVo.setUserUsedCoupons(usedCouponVo);
        }

        if (CollectionUtils.isNotEmpty(userExpiredCoupons)) {
            Map<Long, CouponTypeRo> finalCouponTypeRoMap = couponTypeRoMap;
            List<CouponVo> expiredCouponVo = userExpiredCoupons.stream().filter(ro -> {
                CouponTypeRo couponTypeRo = finalCouponTypeRoMap.get(ro.getCouponTypeId());
                return this.isCouponUsableNow(couponTypeRo, ro);
            }).map(ro -> new CouponRo2CouponVo(finalCouponTypeRoMap.get(ro.getCouponTypeId())).apply(ro)).filter(Objects::nonNull).collect(Collectors.toList());
            couponListVo.setUserExpiredCoupons(expiredCouponVo);
        }

        return couponListVo;
    }

    @Override
    public List<CouponVo> getCoupons4OrderProducts(GetOrderProductAvailableCouponReqVo reqVo) {
        Long userId = reqVo.getUserId();
        List<GetOrderProductAvailableCouponProductItemVo> reqVoItems = reqVo.getItems();
        List<CouponRo> couponRos = couponRedisDao.listUserUsedCoupons(userId);
        if (CollectionUtils.isEmpty(couponRos)) {
            return Lists.newArrayList();
        }
        List<CouponTypeRo> couponTypeRos = couponTypeRedisDao.findByIds(couponRos.stream().map(CouponRo::getCouponTypeId).collect(Collectors.toSet()));
        final Map<Long, CouponTypeRo> couponTypeRoMap = couponTypeRos.stream().collect(Collectors.toMap(CouponTypeRo::getId, Function.identity()));

        List<CouponVo> couponVos = Lists.newArrayList();
        for (CouponRo couponRo : couponRos) {
            CouponTypeRo couponTypeRo = couponTypeRoMap.get(couponRo.getCouponTypeId());
            if (this.isCouponRoAvailable4OrderProducts(couponTypeRo, reqVoItems) && this.isCouponUsableNow(couponTypeRo, couponRo)) {
                CouponRo2CouponVo couponRo2CouponVo = new CouponRo2CouponVo(couponTypeRo);
                couponVos.add(couponRo2CouponVo.apply(couponRo));
            }
        }
        return couponVos;
    }

    @Override
    public Integer getCouponCutAmount4OrderProducts(GetCouponCutAmountReqVo reqVo) {
        Long couponId = reqVo.getCouponId();
        Long userId = reqVo.getUserId();
        List<GetOrderProductAvailableCouponProductItemVo> items = reqVo.getItems();
        AssertUtils.isTrue(couponRedisDao.isCouponBelong2User(userId, couponId), CouponExceptionType.COUPON_NOT_BELONG_TO_USER);
        CouponRo couponRo = couponRedisDao.findOne(couponId);
        AssertUtils.isTrue(Objects.nonNull(couponRo.getCouponTypeId()), CouponExceptionType.COUPON_STATUS_ILLEGAL);
        final CouponTypeRo couponTypeRo = couponTypeRedisDao.findOne(couponRo.getCouponTypeId());
        Set<Long> includeCategories = Arrays.stream(StringUtils.split(Optional.ofNullable(couponTypeRo.getCategories()).orElse(""), ",")).map(Long::valueOf).collect(Collectors.toSet());
        Set<Long> includeBrands = Arrays.stream(StringUtils.split(Optional.ofNullable(couponTypeRo.getBrands()).orElse(""), ",")).map(Long::valueOf).collect(Collectors.toSet());
        Set<Long> includeVendors = Arrays.stream(StringUtils.split(Optional.ofNullable(couponTypeRo.getVendors()).orElse(""), ",")).map(Long::valueOf).collect(Collectors.toSet());
        Set<Long> includeProducts = Arrays.stream(StringUtils.split(Optional.ofNullable(couponTypeRo.getIncludeProducts()).orElse(""), ",")).map(Long::valueOf).collect(Collectors.toSet());
        Set<Long> excludeProducts = Arrays.stream(StringUtils.split(Optional.ofNullable(couponTypeRo.getExcludeProducts()).orElse(""), ",")).map(Long::valueOf).collect(Collectors.toSet());
        Integer collectedProductTotalAmount = Optional.ofNullable(items).orElse(Lists.newArrayList()).stream()
                .filter(item -> {
                    CouponLimitType couponLimitType = couponTypeRo.getCouponLimitType();
                    CouponScopeType couponScopeType = couponTypeRo.getCouponScopeType();
                    Boolean isAvailableInScope = (couponScopeType != CouponScopeType.VENDOR) || (CollectionUtils.isNotEmpty(includeVendors) && includeVendors.contains(item.getVendorId()));
                    if (couponLimitType == CouponLimitType.ALL) {
                        return isAvailableInScope;
                    } else if (couponLimitType == CouponLimitType.CATEGORY_LIMIT) {
                        return CollectionUtils.isNotEmpty(includeCategories) && includeCategories.contains(item.getCategoryId()) && isAvailableInScope;
                    } else if (couponLimitType == CouponLimitType.BRAND_LIMIT) {
                        return CollectionUtils.isNotEmpty(includeBrands) && includeBrands.contains(item.getBrandId()) && isAvailableInScope;
                    } else {
                        return CollectionUtils.isNotEmpty(includeProducts) && includeProducts.contains(item.getProductId())
                                && (CollectionUtils.isEmpty(excludeProducts) || (CollectionUtils.isNotEmpty(excludeProducts) && !excludeProducts.contains(item.getProductId())))
                                && isAvailableInScope;
                    }
                }).map(GetOrderProductAvailableCouponProductItemVo::getSubTotal).reduce(0, (a, b) -> a + b);
        return collectedProductTotalAmount >= couponTypeRo.getCouponLimitAmount() ? Math.max(collectedProductTotalAmount - couponTypeRo.getFaceValue(), 0) : 0;
    }

    @Override
    public void useCoupon(UseCouponReqVo reqVo) {
        Long couponId = reqVo.getCouponId();
        Long userId = reqVo.getUserId();
        AssertUtils.isTrue(couponRedisDao.isCouponBelong2User(userId, couponId), CouponExceptionType.COUPON_NOT_BELONG_TO_USER);
        CouponRo couponRo = couponRedisDao.findOne(couponId);
        couponRo.setCouponState(CouponState.USED);
        couponRo.setUseTime(LocalDateTime.now());
        couponRo.setUpdateTimestamp(new Timestamp(System.currentTimeMillis()));
        couponRedisDao.save(couponRo);
        AssertUtils.notNull(couponRo, CouponExceptionType.COUPON_STATUS_ILLEGAL);
        couponRedisDao.useUserCoupon(userId, couponId);
    }

    private Integer getOrderProductItemsSubTotal(Collection<GetOrderProductAvailableCouponProductItemVo> itemVos, CouponScopeType couponScopeType, Collection<Long> vendorIds) {
        if (CollectionUtils.isEmpty(itemVos)) {
            return 0;
        }
        if (couponScopeType == CouponScopeType.PLATFORM) {
            return itemVos.stream().map(GetOrderProductAvailableCouponProductItemVo::getSubTotal).reduce(0, (a, b) -> a + b);
        } else {
            if (CollectionUtils.isEmpty(vendorIds)) {
                return 0;
            } else {
                return itemVos.stream().filter(vo -> vendorIds.contains(vo.getVendorId())).map(GetOrderProductAvailableCouponProductItemVo::getSubTotal).reduce(0, (a, b) -> a + b);
            }
        }
    }

    private boolean isCouponRoAvailable4OrderProducts(CouponTypeRo couponTypeRo, List<GetOrderProductAvailableCouponProductItemVo> orderProductItems) {
        if (Objects.isNull(couponTypeRo) || Objects.isNull(couponTypeRo.getCouponScopeType())) {
            return false;
        }
        CouponScopeType couponScopeType = couponTypeRo.getCouponScopeType();
        Map<Long, List<GetOrderProductAvailableCouponProductItemVo>> categoryProductItemMap = Maps.newHashMap();
        Map<Long, List<GetOrderProductAvailableCouponProductItemVo>> brandProductItemMap = Maps.newHashMap();
        Map<Long, List<GetOrderProductAvailableCouponProductItemVo>> vendorProductItemMap = Maps.newHashMap();
        Integer orderProductTotalSum = 0;
        for (GetOrderProductAvailableCouponProductItemVo vo : orderProductItems) {
            orderProductTotalSum = orderProductTotalSum + vo.getSubTotal();
            if (Objects.nonNull(vo.getCategoryId())) {
                List<GetOrderProductAvailableCouponProductItemVo> categoryProductItems = categoryProductItemMap.getOrDefault(vo.getCategoryId(), Lists.newArrayList());
                categoryProductItems.add(vo);
                categoryProductItemMap.put(vo.getCategoryId(), categoryProductItems);
            }
            if (Objects.nonNull(vo.getBrandId())) {
                List<GetOrderProductAvailableCouponProductItemVo> brandProductItems = brandProductItemMap.getOrDefault(vo.getBrandId(), Lists.newArrayList());
                brandProductItems.add(vo);
                brandProductItemMap.put(vo.getBrandId(), brandProductItems);
            }
            if (Objects.nonNull(vo.getVendorId())) {
                List<GetOrderProductAvailableCouponProductItemVo> vendorProductItems = vendorProductItemMap.getOrDefault(vo.getVendorId(), Lists.newArrayList());
                vendorProductItems.add(vo);
                vendorProductItemMap.put(vo.getVendorId(), vendorProductItems);
            }
        }
        CouponLimitType couponLimitType = couponTypeRo.getCouponLimitType();
        Integer couponAmountLimit = ValueUtils.getValue(couponTypeRo.getCouponLimitAmount());
        Set<Long> includeVendors = Arrays.stream(StringUtils.split(Optional.ofNullable(couponTypeRo.getVendors()).orElse(""), ",")).map(Long::valueOf).collect(Collectors.toSet());
        Collection<Long> retainVendorIds = CollectionUtils.retainAll(includeVendors, vendorProductItemMap.keySet());
        if (couponLimitType == CouponLimitType.ALL) {
            Integer orderProductCalcAmount;
            if (couponScopeType == CouponScopeType.PLATFORM) {
                orderProductCalcAmount = orderProductTotalSum;
            } else {
                if (CollectionUtils.isEmpty(retainVendorIds)) {
                    return false;
                }
                orderProductCalcAmount = this.getOrderProductItemsSubTotal(orderProductItems, CouponScopeType.VENDOR, retainVendorIds);
            }
            if (orderProductCalcAmount < couponAmountLimit) {
                return false;
            }
        } else if (couponLimitType == CouponLimitType.CATEGORY_LIMIT) {
            Set<Long> includeCategories = Arrays.stream(StringUtils.split(Optional.ofNullable(couponTypeRo.getCategories()).orElse(""), ",")).map(Long::valueOf).collect(Collectors.toSet());
            Collection<Long> retainCategories = CollectionUtils.retainAll(includeCategories, categoryProductItemMap.keySet());
            if (CollectionUtils.isNotEmpty(includeCategories) && CollectionUtils.isEmpty(retainCategories)) {
                return false;
            }
            Integer categorySubTotal = 0;
            if (couponScopeType == CouponScopeType.PLATFORM) {
                for (Long includeCategory : includeCategories) {
                    List<GetOrderProductAvailableCouponProductItemVo> categoryItems = categoryProductItemMap.get(includeCategory);
                    categorySubTotal = categorySubTotal + this.getOrderProductItemsSubTotal(categoryItems, CouponScopeType.PLATFORM, Collections.emptyList());
                }
            } else {
                if (CollectionUtils.isEmpty(retainVendorIds)) {
                    return false;
                }
                for (Long includeCategory : includeCategories) {
                    List<GetOrderProductAvailableCouponProductItemVo> categoryItems = categoryProductItemMap.get(includeCategory);
                    categorySubTotal = categorySubTotal + this.getOrderProductItemsSubTotal(categoryItems, CouponScopeType.VENDOR, retainVendorIds);
                }
            }
            if (categorySubTotal < couponAmountLimit) {
                return false;
            }
        } else if (couponLimitType == CouponLimitType.BRAND_LIMIT) {
            Set<Long> includeBrands = Arrays.stream(StringUtils.split(Optional.ofNullable(couponTypeRo.getBrands()).orElse(""), ",")).map(Long::valueOf).collect(Collectors.toSet());
            Collection<Long> retainBrands = CollectionUtils.retainAll(includeBrands, brandProductItemMap.keySet());
            if (CollectionUtils.isNotEmpty(includeBrands) && CollectionUtils.isEmpty(retainBrands)) {
                return false;
            }
            Integer brandSubTotal = 0;
            if (couponScopeType == CouponScopeType.PLATFORM) {
                for (Long includeBrand : includeBrands) {
                    List<GetOrderProductAvailableCouponProductItemVo> brandItems = brandProductItemMap.get(includeBrand);
                    brandSubTotal = brandSubTotal + this.getOrderProductItemsSubTotal(brandItems, CouponScopeType.PLATFORM, Collections.emptyList());
                }
            } else {
                if (CollectionUtils.isEmpty(retainVendorIds)) {
                    return false;
                }
                for (Long includeBrand : includeBrands) {
                    List<GetOrderProductAvailableCouponProductItemVo> brandItems = brandProductItemMap.get(includeBrand);
                    brandSubTotal = brandSubTotal + this.getOrderProductItemsSubTotal(brandItems, CouponScopeType.VENDOR, retainVendorIds);
                }
            }

            if (brandSubTotal < couponAmountLimit) {
                return false;
            }
        } else {
            Set<Long> includeProducts = Arrays.stream(StringUtils.split(Optional.ofNullable(couponTypeRo.getIncludeProducts()).orElse(""), ",")).map(Long::valueOf).collect(Collectors.toSet());
            Set<Long> allOrderProductIds = orderProductItems.stream().map(GetOrderProductAvailableCouponProductItemVo::getProductId).collect(Collectors.toSet());
            Collection<Long> retainOrderProductIds = CollectionUtils.retainAll(includeProducts, allOrderProductIds);
            if (CollectionUtils.isEmpty(retainOrderProductIds)) {
                return false;
            }
            Integer productSubTotal;
            if (couponScopeType == CouponScopeType.PLATFORM) {
                productSubTotal = orderProductItems.stream().filter(vo -> retainOrderProductIds.contains(vo.getProductId())).map(GetOrderProductAvailableCouponProductItemVo::getSubTotal).reduce(0, (a, b) -> a + b);
            } else {
                productSubTotal = orderProductItems.stream().filter(vo -> retainOrderProductIds.contains(vo.getProductId()) && retainVendorIds.contains(vo.getVendorId())).map(GetOrderProductAvailableCouponProductItemVo::getSubTotal).reduce(0, (a, b) -> a + b);
            }
            if (productSubTotal < couponAmountLimit) {
                return false;
            }
        }
        return true;
    }

    private boolean isCouponUsableNow(CouponTypeRo couponTypeRo, CouponRo couponRo) {
        LocalDateTime now = LocalDateTime.now();
        LocalDateTime usableStartTime = couponTypeRo.getUsableStartTime();
        LocalDateTime usableEndTime = couponTypeRo.getUsableEndTime();
        if (Objects.isNull(couponRo) || couponRo.getUsed() == Boolean.TRUE || Objects.isNull(couponRo.getReceivedTime()) || Objects.isNull(usableStartTime) || Objects.isNull(usableEndTime)) {
            return false;
        }

        if (Objects.nonNull(couponTypeRo.getUsableCountDownDays())) {
            LocalDateTime couponUnavailableTimeInCountDown = couponRo.getReceivedTime().plusDays(couponTypeRo.getUsableCountDownDays());
            usableEndTime = couponUnavailableTimeInCountDown.isBefore(usableEndTime) ? couponUnavailableTimeInCountDown : usableEndTime;
        }
        return now.isBefore(usableEndTime) && now.isAfter(usableStartTime);
    }
}
